/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { expect } from 'chai';
import { describe, it } from 'mocha';
import { Source } from '../source';
import { createLexer, TokenKind } from '../lexer';

function lexOne(str) {
  const lexer = createLexer(new Source(str));
  return lexer.advance();
}

function expectSyntaxError(text, message, location) {
  try {
    lexOne(text);
    expect.fail('Expected to throw syntax error');
  } catch (error) {
    expect(error.message).to.contain(message);
    expect(error.locations).to.deep.equal([location]);
  }
}

describe('Lexer', () => {
  it('disallows uncommon control characters', () => {
    expectSyntaxError(
      '\u0007',
      'Cannot contain the invalid character "\\u0007"',
      { line: 1, column: 1 },
    );
  });

  it('accepts BOM header', () => {
    expect(lexOne('\uFEFF foo')).to.containSubset({
      kind: TokenKind.NAME,
      start: 2,
      end: 5,
      value: 'foo',
    });
  });

  it('records line and column', () => {
    expect(lexOne('\n \r\n \r  foo\n')).to.containSubset({
      kind: TokenKind.NAME,
      start: 8,
      end: 11,
      line: 4,
      column: 3,
      value: 'foo',
    });
  });

  it('can be JSON.stringified or util.inspected', () => {
    const token = lexOne('foo');
    expect(JSON.stringify(token)).to.equal(
      '{"kind":"Name","value":"foo","line":1,"column":1}',
    );
    // NB: util.inspect used to suck
    if (parseFloat(process.version.slice(1)) > 0.1) {
      expect(require('util').inspect(token)).to.equal(
        "{ kind: 'Name', value: 'foo', line: 1, column: 1 }",
      );
    }
  });

  it('skips whitespace and comments', () => {
    expect(
      lexOne(`

    foo


`),
    ).to.containSubset({
      kind: TokenKind.NAME,
      start: 6,
      end: 9,
      value: 'foo',
    });

    expect(
      lexOne(`
    #comment
    foo#comment
`),
    ).to.containSubset({
      kind: TokenKind.NAME,
      start: 18,
      end: 21,
      value: 'foo',
    });

    expect(lexOne(',,,foo,,,')).to.containSubset({
      kind: TokenKind.NAME,
      start: 3,
      end: 6,
      value: 'foo',
    });
  });

  it('errors respect whitespace', () => {
    let caughtError;
    try {
      lexOne(`

    ?


`);
    } catch (error) {
      caughtError = error;
    }
    expect(String(caughtError)).to.equal(
      'Syntax Error: Cannot parse the unexpected character "?".\n' +
        '\n' +
        'GraphQL request (3:5)\n' +
        '2: \n' +
        '3:     ?\n' +
        '       ^\n' +
        '4: \n',
    );
  });

  it('updates line numbers in error for file context', () => {
    let caughtError;
    try {
      const str = '' + '\n' + '\n' + '     ?\n' + '\n';
      const source = new Source(str, 'foo.js', { line: 11, column: 12 });
      createLexer(source).advance();
    } catch (error) {
      caughtError = error;
    }
    expect(String(caughtError)).to.equal(
      'Syntax Error: Cannot parse the unexpected character "?".\n' +
        '\n' +
        'foo.js (13:6)\n' +
        '12: \n' +
        '13:      ?\n' +
        '         ^\n' +
        '14: \n',
    );
  });

  it('updates column numbers in error for file context', () => {
    let caughtError;
    try {
      const source = new Source('?', 'foo.js', { line: 1, column: 5 });
      createLexer(source).advance();
    } catch (error) {
      caughtError = error;
    }
    expect(String(caughtError)).to.equal(
      'Syntax Error: Cannot parse the unexpected character "?".\n' +
        '\n' +
        'foo.js (1:5)\n' +
        '1:     ?\n' +
        '       ^\n',
    );
  });

  it('lexes strings', () => {
    expect(lexOne('"simple"')).to.containSubset({
      kind: TokenKind.STRING,
      start: 0,
      end: 8,
      value: 'simple',
    });

    expect(lexOne('" white space "')).to.containSubset({
      kind: TokenKind.STRING,
      start: 0,
      end: 15,
      value: ' white space ',
    });

    expect(lexOne('"quote \\""')).to.containSubset({
      kind: TokenKind.STRING,
      start: 0,
      end: 10,
      value: 'quote "',
    });

    expect(lexOne('"escaped \\n\\r\\b\\t\\f"')).to.containSubset({
      kind: TokenKind.STRING,
      start: 0,
      end: 20,
      value: 'escaped \n\r\b\t\f',
    });

    expect(lexOne('"slashes \\\\ \\/"')).to.containSubset({
      kind: TokenKind.STRING,
      start: 0,
      end: 15,
      value: 'slashes \\ /',
    });

    expect(lexOne('"unicode \\u1234\\u5678\\u90AB\\uCDEF"')).to.containSubset({
      kind: TokenKind.STRING,
      start: 0,
      end: 34,
      value: 'unicode \u1234\u5678\u90AB\uCDEF',
    });
  });

  it('lex reports useful string errors', () => {
    expectSyntaxError('"', 'Unterminated string.', { line: 1, column: 2 });

    expectSyntaxError('"no end quote', 'Unterminated string.', {
      line: 1,
      column: 14,
    });

    expectSyntaxError(
      "'single quotes'",
      "Unexpected single quote character ('), " +
        'did you mean to use a double quote (")?',
      { line: 1, column: 1 },
    );

    expectSyntaxError(
      '"contains unescaped \u0007 control char"',
      'Invalid character within String: "\\u0007".',
      { line: 1, column: 21 },
    );

    expectSyntaxError(
      '"null-byte is not \u0000 end of file"',
      'Invalid character within String: "\\u0000".',
      { line: 1, column: 19 },
    );

    expectSyntaxError('"multi\nline"', 'Unterminated string', {
      line: 1,
      column: 7,
    });

    expectSyntaxError('"multi\rline"', 'Unterminated string', {
      line: 1,
      column: 7,
    });

    expectSyntaxError(
      '"bad \\z esc"',
      'Invalid character escape sequence: \\z.',
      { line: 1, column: 7 },
    );

    expectSyntaxError(
      '"bad \\x esc"',
      'Invalid character escape sequence: \\x.',
      { line: 1, column: 7 },
    );

    expectSyntaxError(
      '"bad \\u1 esc"',
      'Invalid character escape sequence: \\u1 es.',
      { line: 1, column: 7 },
    );

    expectSyntaxError(
      '"bad \\u0XX1 esc"',
      'Invalid character escape sequence: \\u0XX1.',
      { line: 1, column: 7 },
    );

    expectSyntaxError(
      '"bad \\uXXXX esc"',
      'Invalid character escape sequence: \\uXXXX.',
      { line: 1, column: 7 },
    );

    expectSyntaxError(
      '"bad \\uFXXX esc"',
      'Invalid character escape sequence: \\uFXXX.',
      { line: 1, column: 7 },
    );

    expectSyntaxError(
      '"bad \\uXXXF esc"',
      'Invalid character escape sequence: \\uXXXF.',
      { line: 1, column: 7 },
    );
  });

  it('lexes block strings', () => {
    expect(lexOne('"""simple"""')).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 12,
      value: 'simple',
    });

    expect(lexOne('""" white space """')).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 19,
      value: ' white space ',
    });

    expect(lexOne('"""contains " quote"""')).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 22,
      value: 'contains " quote',
    });

    expect(lexOne('"""contains \\""" triplequote"""')).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 31,
      value: 'contains """ triplequote',
    });

    expect(lexOne('"""multi\nline"""')).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 16,
      value: 'multi\nline',
    });

    expect(lexOne('"""multi\rline\r\nnormalized"""')).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 28,
      value: 'multi\nline\nnormalized',
    });

    expect(lexOne('"""unescaped \\n\\r\\b\\t\\f\\u1234"""')).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 32,
      value: 'unescaped \\n\\r\\b\\t\\f\\u1234',
    });

    expect(lexOne('"""slashes \\\\ \\/"""')).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 19,
      value: 'slashes \\\\ \\/',
    });

    expect(
      lexOne(`"""

        spans
          multiple
            lines

        """`),
    ).to.containSubset({
      kind: TokenKind.BLOCK_STRING,
      start: 0,
      end: 68,
      value: 'spans\n  multiple\n    lines',
    });
  });

  it('lex reports useful block string errors', () => {
    expectSyntaxError('"""', 'Unterminated string.', { line: 1, column: 4 });

    expectSyntaxError('"""no end quote', 'Unterminated string.', {
      line: 1,
      column: 16,
    });

    expectSyntaxError(
      '"""contains unescaped \u0007 control char"""',
      'Invalid character within String: "\\u0007".',
      { line: 1, column: 23 },
    );

    expectSyntaxError(
      '"""null-byte is not \u0000 end of file"""',
      'Invalid character within String: "\\u0000".',
      { line: 1, column: 21 },
    );
  });

  it('lexes numbers', () => {
    expect(lexOne('4')).to.containSubset({
      kind: TokenKind.INT,
      start: 0,
      end: 1,
      value: '4',
    });

    expect(lexOne('4.123')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 5,
      value: '4.123',
    });

    expect(lexOne('-4')).to.containSubset({
      kind: TokenKind.INT,
      start: 0,
      end: 2,
      value: '-4',
    });

    expect(lexOne('9')).to.containSubset({
      kind: TokenKind.INT,
      start: 0,
      end: 1,
      value: '9',
    });

    expect(lexOne('0')).to.containSubset({
      kind: TokenKind.INT,
      start: 0,
      end: 1,
      value: '0',
    });

    expect(lexOne('-4.123')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 6,
      value: '-4.123',
    });

    expect(lexOne('0.123')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 5,
      value: '0.123',
    });

    expect(lexOne('123e4')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 5,
      value: '123e4',
    });

    expect(lexOne('123E4')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 5,
      value: '123E4',
    });

    expect(lexOne('123e-4')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 6,
      value: '123e-4',
    });

    expect(lexOne('123e+4')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 6,
      value: '123e+4',
    });

    expect(lexOne('-1.123e4')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 8,
      value: '-1.123e4',
    });

    expect(lexOne('-1.123E4')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 8,
      value: '-1.123E4',
    });

    expect(lexOne('-1.123e-4')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 9,
      value: '-1.123e-4',
    });

    expect(lexOne('-1.123e+4')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 9,
      value: '-1.123e+4',
    });

    expect(lexOne('-1.123e4567')).to.containSubset({
      kind: TokenKind.FLOAT,
      start: 0,
      end: 11,
      value: '-1.123e4567',
    });
  });

  it('lex reports useful number errors', () => {
    expectSyntaxError('00', 'Invalid number, unexpected digit after 0: "0".', {
      line: 1,
      column: 2,
    });

    expectSyntaxError('+1', 'Cannot parse the unexpected character "+".', {
      line: 1,
      column: 1,
    });

    expectSyntaxError('1.', 'Invalid number, expected digit but got: <EOF>.', {
      line: 1,
      column: 3,
    });

    expectSyntaxError('1.e1', 'Invalid number, expected digit but got: "e".', {
      line: 1,
      column: 3,
    });

    expectSyntaxError('.123', 'Cannot parse the unexpected character ".".', {
      line: 1,
      column: 1,
    });

    expectSyntaxError('1.A', 'Invalid number, expected digit but got: "A".', {
      line: 1,
      column: 3,
    });

    expectSyntaxError('-A', 'Invalid number, expected digit but got: "A".', {
      line: 1,
      column: 2,
    });

    expectSyntaxError(
      '1.0e',
      'Invalid number, expected digit but got: <EOF>.',
      { line: 1, column: 5 },
    );

    expectSyntaxError('1.0eA', 'Invalid number, expected digit but got: "A".', {
      line: 1,
      column: 5,
    });
  });

  it('lexes punctuation', () => {
    expect(lexOne('!')).to.containSubset({
      kind: TokenKind.BANG,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('$')).to.containSubset({
      kind: TokenKind.DOLLAR,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('(')).to.containSubset({
      kind: TokenKind.PAREN_L,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne(')')).to.containSubset({
      kind: TokenKind.PAREN_R,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('...')).to.containSubset({
      kind: TokenKind.SPREAD,
      start: 0,
      end: 3,
      value: undefined,
    });

    expect(lexOne(':')).to.containSubset({
      kind: TokenKind.COLON,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('=')).to.containSubset({
      kind: TokenKind.EQUALS,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('@')).to.containSubset({
      kind: TokenKind.AT,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('[')).to.containSubset({
      kind: TokenKind.BRACKET_L,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne(']')).to.containSubset({
      kind: TokenKind.BRACKET_R,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('{')).to.containSubset({
      kind: TokenKind.BRACE_L,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('|')).to.containSubset({
      kind: TokenKind.PIPE,
      start: 0,
      end: 1,
      value: undefined,
    });

    expect(lexOne('}')).to.containSubset({
      kind: TokenKind.BRACE_R,
      start: 0,
      end: 1,
      value: undefined,
    });
  });

  it('lex reports useful unknown character error', () => {
    expectSyntaxError('..', 'Cannot parse the unexpected character ".".', {
      line: 1,
      column: 1,
    });

    expectSyntaxError('?', 'Cannot parse the unexpected character "?".', {
      line: 1,
      column: 1,
    });

    expectSyntaxError(
      '\u203B',
      'Cannot parse the unexpected character "\\u203B".',
      { line: 1, column: 1 },
    );

    expectSyntaxError(
      '\u200b',
      'Cannot parse the unexpected character "\\u200B".',
      { line: 1, column: 1 },
    );
  });

  it('lex reports useful information for dashes in names', () => {
    const q = 'a-b';
    const lexer = createLexer(new Source(q));
    const firstToken = lexer.advance();
    expect(firstToken).to.containSubset({
      kind: TokenKind.NAME,
      start: 0,
      end: 1,
      value: 'a',
    });
    let caughtError;
    try {
      lexer.advance();
    } catch (error) {
      caughtError = error;
    }
    expect(caughtError.message).to.equal(
      'Syntax Error: Invalid number, expected digit but got: "b".',
    );
    expect(caughtError.locations).to.deep.equal([{ line: 1, column: 3 }]);
  });

  it('produces double linked list of tokens, including comments', () => {
    const lexer = createLexer(
      new Source(`{
      #comment
      field
    }`),
    );

    const startToken = lexer.token;
    let endToken;
    do {
      endToken = lexer.advance();
      // Lexer advances over ignored comment tokens to make writing parsers
      // easier, but will include them in the linked list result.
      expect(endToken.kind).not.to.equal('Comment');
    } while (endToken.kind !== '<EOF>');

    expect(startToken.prev).to.equal(null);
    expect(endToken.next).to.equal(null);

    const tokens = [];
    for (let tok = startToken; tok; tok = tok.next) {
      if (tokens.length) {
        // Tokens are double-linked, prev should point to last seen token.
        expect(tok.prev).to.equal(tokens[tokens.length - 1]);
      }
      tokens.push(tok);
    }

    expect(tokens.map(tok => tok.kind)).to.deep.equal([
      '<SOF>',
      '{',
      'Comment',
      'Name',
      '}',
      '<EOF>',
    ]);
  });
});
